package xyhoo.springframework.util.xml;

import xyhoo.springframework.util.StringUtils;

import java.io.*;

/**
 * 检测一个XML数据流是使用DTD还是XSD进行基于模式的验证
 */
public class XmlValidationModeDetector {

    /**
     * 禁用验证
     */
    public static final int VALIDATION_NONE = 0;

    /**
     * 自动推断验证模式，因为无法找到明确的指示
     */
    public static final int VALIDATION_AUTO = 1;

    /**
     * DTD验证模式，因为找到一个"DOCTYPE"声明
     */
    public static final int VALIDATION_DTD = 2;

    /**
     * XSD验证模式，因为没有找到"DOCTYPE"声明
     */
    public static final int VALIDATION_XSD = 3;

    private static final String DOCTYPE = "DOCTYPE";

    private static final String START_COMMENT = "<!--";

    private static final String END_COMMENT = "-->";

    /**
     * 当前解析位置是否处于XML注释内部
     */
    private boolean inComment;

    /**
     * 检测所提供的{@link InputStream}中XML文件校验模式。注意，此方法会在返回前关闭{@link InputStream}资源
     *
     * @param inputStream
     * @return
     * @throws IOException
     */
    public int detectValidationMode(InputStream inputStream) throws IOException {
        this.inComment = false;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            boolean isDtdValidated = false;
            String content;
            while ((content = reader.readLine()) != null) {
                content = consumeCommentTokens(content);
                if (!StringUtils.hasText(content)) {
                    continue;
                }
                if (hasDoctype(content)) {
                    isDtdValidated = true;
                    break;
                }
                if (hasOpeningTag(content)) {
                    break;
                }
            }
            return isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD;
        } catch (CharConversionException ex) {
            return VALIDATION_AUTO;
        }
    }

    private boolean hasDoctype(String content) {
        return content.contains(DOCTYPE);
    }

    /**
     * 判断所提供的内容中是否含有XML开始标签（也就是'<'标签）。在将剩余内容传递给此方法之前，预期所有注释部分已经移除。
     * 但是，作为一项完整性检测，如果当前解析状态正处于注释状态，该方法直接返回{@code false}
     *
     * @param content
     * @return
     */
    protected boolean hasOpeningTag(String content) {
        if (this.inComment) {
            return false;
        }
        int openTagIndex = content.indexOf('<');
        return openTagIndex > -1 && (content.length() > openTagIndex + 1) && Character.isLetter(content.charAt(openTagIndex + 1));
    }

    /**
     * 移除给定字符串中的注释，并返回剩余内容，由于输入内容可能全部是注释，因此返回结果可能是空字符串。
     * 该方法会考虑当前处于"注释中"的解析状态
     *
     * @param line
     * @return
     */
    private String consumeCommentTokens(String line) {
        int indexOfStartComment = line.indexOf(START_COMMENT);
        if (indexOfStartComment == -1 && !line.contains(END_COMMENT)) {
            return line;
        }

        String result = "";
        String currLine = line;
        if (!this.inComment && (indexOfStartComment >= 0)) {
            result = line.substring(0, indexOfStartComment);
            currLine = line.substring(indexOfStartComment);
        }

        if ((currLine = consume(currLine)) != null) {
            result += consumeCommentTokens(currLine);
        }
        return result;
    }

    private String consume(String line) {
        int index = this.inComment ? endComment(line) : startComment(line);
        return index == -1 ? null : line.substring(index);
    }

    private int endComment(String line) {
        return commentToken(line, END_COMMENT, false);
    }

    private int startComment(String line) {
        return commentToken(line, START_COMMENT, true);
    }

    private int commentToken(String line, String token, boolean inCommentIfPresent) {
        int index = line.indexOf(token);
        if (index > -1) {
            this.inComment = inCommentIfPresent;
        }
        return index == -1 ? index : index + token.length();
    }

}
